package com.example.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.common.constant.RedisConstant;
import com.example.common.utils.PageUtils;
import com.example.common.utils.Query;
import com.example.gulimall.product.dao.CategoryDao;
import com.example.gulimall.product.entity.CategoryEntity;
import com.example.gulimall.product.service.CategoryBrandRelationService;
import com.example.gulimall.product.service.CategoryService;
import com.example.gulimall.product.vo.Catelog2Vo;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    RedissonClient redisson;


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );
        return new PageUtils(page);
    }

    /**
     * 查询所有的一级分类信息
     *  缓存原理
     *   CacheAutoConfiguration导入缓存（CacheConfigurationImportSelector-getConfigurationClass）
     *   ->RedisCacheConfiguration->自动配置了RedisCacheManager->初始化所有的缓存
     *   ->每个缓存决定使用什么配置->如果determineConfiguration配置有就用现有的，没有就用默认(redis)的
     *   ->想要修改缓存配置，只需要系统容器中配置一个RedisCacheConfiguration即可
     *   ->就会用到RedisCacheManager所有的缓存分区
     *  原理：
     *     CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
     * @return java.util.List<com.example.gulimall.product.entity.CategoryEntity>
     * @author GuLin
     * @date 9:32 2021/6/16
     **/
    @Override
    @Cacheable(value = {"category_key"},key = "#root.method.name",sync = true)
    public List<CategoryEntity> getLevel1Catagories() {
        List<CategoryEntity> entityList = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return entityList;
    }
    @Override
    @Cacheable(value ={"category_key"},key = "#root.method.name",sync = true)
    public Map<String, List<Catelog2Vo>> getCatelogJsonFroSpringCache(){

        return this.getCatelogJsonFroDb();
    }

    /**
     * 修改分类，并且更新品牌信息
     *  @Cacheable(value ={"category_key"},key = "#root.method.name",sync = true)
     *  value 相当于换缓存分区，key 缓存key值 ，开启同步
     *   @CachePut 更新缓存而不干扰方法执行。根据当前方法获取返回值进行更新
     *   @CacheEvict(value = "category_key",key = "'getLevel1Catagories'")
     *     单个删除缓存，key赋值的属性如果是字符串记得加单引号
     *   @Caching(evict = {
     *          @CacheEvict(value = "category_key",key = "'getLevel1Catagories'"),
     *          @CacheEvict(value = "category_key",key = "'getCatelogJsonFroSpringCache'")
     *     })
     *    删除多个方式1
     *  @CacheEvict(value = "category_key",allEntries = true)
     *    删除多个方式2
     * @param category
     */
    @Transactional
    @Override
//    @Caching(evict = {
//         @CacheEvict(value = "category_key",key = "'getLevel1Catagories'"),
//         @CacheEvict(value = "category_key",key = "'getCatelogJsonFroSpringCache'")
//    })
    @CacheEvict(value = "category_key",allEntries = true)
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }


    /**
     * 查询所有子菜单
     * 增加redis缓存
     * 问题
     *    1.OutOfDirectMemoryError: 堆外内存溢出
     *    由于是lettuce的bug造成，不要直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存，治标不治本。
     *    logger.debug("-Dio.netty.maxDirectMemory: {} bytes", maxDirectMemory);
     *    DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;
     * 解决
     *   直接引用jedis，详情查看pom.xml
     * 单机本地锁  synchronized (this)/JUC(look)
     * @return java.util.Map<java.lang.String, java.util.List < com.example.gulimall.product.vo.Catelog2Vo>>
     * @author GuLin
     * @date 10:10 2021/6/16
     **/
    public Map<String, List<Catelog2Vo>> getCatelogJsonRedis() {
       //查询缓存是否存在菜单信息
        String result = redisTemplate.opsForValue().get(RedisConstant.CATEGORY_KEY);
        //如果缓存为空则查询数据操作
        if(StringUtils.isEmpty(result)){
            Map<String, List<Catelog2Vo>> listMap = getCatelogJsonFroDb();
            //存入到缓存，放了方便将其转为json
            String jsonString = JSON.toJSONString(listMap);
            redisTemplate.opsForValue().set(RedisConstant.CATEGORY_KEY,jsonString,1, TimeUnit.DAYS);
            return listMap;
        }
        //因为listMap是一个复杂类型，所以想需要使用必须将它反序列,反序列化TypeReference是一个受保护的方法
        Map<String, List<Catelog2Vo>> listMap = JSON.parseObject(result,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return listMap;
    }
    /**
     * 查询所有子菜单
     * 查询DB
     * @return java.util.Map<java.lang.String, java.util.List < com.example.gulimall.product.vo.Catelog2Vo>>
     * @author GuLin
     * @date 10:10 2021/6/16
     **/
    public Map<String, List<Catelog2Vo>> getCatelogJsonFroDb() {

        System.out.println("查询数据库了.......");
        // 性能优化：将数据库的多次查询变为一次
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);
        //1、查出所有分类
        //1、1）查出所有一级分类
        List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
        //封装数据
        Map<String, List<Catelog2Vo>> categoryMap = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> level2Categories = getParentCid(selectList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catalogs2Vos = null;
            if (null != level2Categories) {
                catalogs2Vos = level2Categories.stream().map(l2 -> {
                    //设置二级菜单信息
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(),  null, l2.getCatId().toString(), l2.getName());
                    // 找当前二级分类的三级分类
                    List<CategoryEntity> level3Categories = getParentCid(selectList, l2.getCatId());
                    if (null != level3Categories) {
                        List<Catelog2Vo.Catelog3Vo> catelog3VoList = level3Categories.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),
                                    l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3VoList);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catalogs2Vos;
        }));
        return categoryMap;
    }

    /**
     * 页面请求方法
     * @return
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        Map<String, List<Catelog2Vo>> result =
                //getCatelogJsonRedis();
                getCatalogJsonFromDbWithRedisLock();
        return result;
    }
    /**
     * Redisson 分布式锁
     * 缓存里的数据如何和数据库的数据保持一致？？
     * 缓存数据一致性，选择2
     *   1)、双写模式
     *   2)、失效模式
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //1、占分布式锁。去redis占坑
        //（锁的粒度，越细越快:具体缓存的是某个数据，11号商品） product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock(RedisConstant.CATEGORY_KEY);
        //创建读锁
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(RedisConstant.CATEGORY_LOCK);
        RLock rLock = readWriteLock.readLock();
        Map<String, List<Catelog2Vo>> listMap = null;
        try {
            rLock.lock();
            //加锁成功 执行业务
            listMap = getCatelogJsonFroDbWithRedisLock();
        } finally {
            rLock.unlock();
        }
        return listMap;
    }


    /**
     * redis 分布式锁
     *    如何解决缓存穿透、雪崩、击穿
     *        1.空结果缓存，解决穿透问题
     *        2.设置过期时间（随机数），解决雪崩问题
     *        3.加锁，解决缓存击穿
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //占分布式锁,置过期时间必须和加锁是同步的，保证原子性（避免死锁）
        String  uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(RedisConstant.LOCK, uuid, 300, TimeUnit.SECONDS);
        if(lock){
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> listMap = null;
            try {
                //加锁成功 执行业务
                listMap = getCatelogJsonFroDbWithRedisLock();
            } finally {
                String lockValue = redisTemplate.opsForValue().get(RedisConstant.LOCK);
               //lua脚本解锁
               String unLock = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
               //删除锁
               redisTemplate.execute(new DefaultRedisScript<>(unLock,Long.class),Collections.singletonList(RedisConstant.LOCK),lockValue);
            }
            //先去redis查询下保证当前的锁是自己的
            //获取值对比，对比成功删除=原子性 lua脚本解锁
            // String lockValue = stringRedisTemplate.opsForValue().get("lock");
            // if (uuid.equals(lockValue)) {
            //     //删除我自己的锁
            //     stringRedisTemplate.delete("lock");
            // }
            return listMap;
        } else {
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...重试机制 并休眠200秒
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //自询方式
            return getCatalogJsonFromDbWithRedisLock();
        }
    }


    /**
     * 查询所有子菜单
     * 查询DB
     * @return java.util.Map<java.lang.String, java.util.List < com.example.gulimall.product.vo.Catelog2Vo>>
     * @author GuLin
     * @date 10:10 2021/6/16
     **/
    public Map<String, List<Catelog2Vo>> getCatelogJsonFroDbWithRedisLock() {
        //判断缓存是否存在
        String result = redisTemplate.opsForValue().get(RedisConstant.CATEGORY_KEY);
        if (!StringUtils.isEmpty(result)) {
            //缓存不为空直接返回
            Map<String, List<Catelog2Vo>> categoryMap = JSON.parseObject(result, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return categoryMap;
        }
        System.out.println("查询数据库了.......");
        // 性能优化：将数据库的多次查询变为一次
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);
        //1、查出所有分类
        //1、1）查出所有一级分类
        List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
        //封装数据
        Map<String, List<Catelog2Vo>> categoryMap = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> level2Categories = getParentCid(selectList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catalogs2Vos = null;
            if (null != level2Categories) {
                catalogs2Vos = level2Categories.stream().map(l2 -> {
                    //设置二级菜单信息
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 找当前二级分类的三级分类
                    List<CategoryEntity> level3Categories = getParentCid(selectList, l2.getCatId());
                    if (null != level3Categories) {
                        List<Catelog2Vo.Catelog3Vo> catelog3VoList = level3Categories.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),
                                    l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3VoList);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catalogs2Vos;
        }));
        //3、将查到的数据放入缓存,将对象转为json
        String valueJson = JSON.toJSONString(categoryMap);
        redisTemplate.opsForValue().set(RedisConstant.CATEGORY_KEY, valueJson, 1, TimeUnit.DAYS);
        return categoryMap;
    }


    private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList, Long parentCid) {
        List<CategoryEntity> entityList = selectList.stream().filter(item -> parentCid.equals(item.getParentCid())).collect(Collectors.toList());
        return entityList;
    }

    @Override
    public List<CategoryEntity> queryCategoryTreeList() {
        //查询所有分离信息
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

        //2、组装成父子的树形结构
        //2.1）、找到所有的一级分类 并排序
        List<CategoryEntity> level1Menus = categoryEntityList.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0)
                .map((menu) -> {
                    menu.setChildren(getChildrens(menu, categoryEntityList));
                    return menu;
                }).sorted((menu1, menu2) -> {
                    //1一级分类排序
                    return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
                }).collect(Collectors.toList());

        return level1Menus;
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity menu, List<CategoryEntity> categoryEntityList) {

        List<CategoryEntity> children = categoryEntityList.stream().filter(categoryEntity -> {
            /**
             * 代码扫描推荐
             * 所有的包装类对象之间值的比较，全部使用equals方法比较。 说明：对于Integer var=?在-128至127之间的赋值，
             * Integer对象是在IntegerCache.cache产生，会复用已有对象，这个区间内的Integer值可以直接使用==进行判断，
             * 但是这个区间之外的所有数据，都会在堆上产生，并不会复用已有对象，这是一个大坑，推荐使用equals方法进行判断。
             */
            return categoryEntity.getParentCid().equals(menu.getCatId());
        }).map(categoryEntity -> {
            //1、找到子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity, categoryEntityList));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            //2.子菜单排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return children;
    }


    /**
     * 根据分类ID获取 完整分类路径
     *
     * @param catelogId
     * @return java.lang.Long[]
     * @author GuLin
     * @date 19:44 2021/6/7
     **/
    @Override
    public Long[] findCatelogPath(Long catelogId) {

        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);

        Collections.reverse(parentPath);

        return parentPath.toArray(new Long[parentPath.size()]);
    }



    //递归查找 225/34/2
    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        //1.收集当前节点
        paths.add(catelogId);
        CategoryEntity entity = this.getById(catelogId);
        if (0 != entity.getParentCid()) {
            //父节点不为0 根据父节点继续查找
            this.findParentPath(entity.getParentCid(), paths);
        }
        return paths;
    }

}